package service

import (
	"encoding/base64"
	"errors"
	"fmt"
	"github.com/gorilla/websocket"
	"github.com/sirupsen/logrus"
	"hcy-api/lib/id"
	"hcy-api/structs/enum"
	"hcy-api/structs/tables"
	"hcy-api/structs/vap"
	"strconv"
	"time"
	"vap/daemon/gua"
	"vap/global"
	"vap/module/dao"
)

var GuaServ = new(guaService)

type guaService struct {
}

func (s guaService) Conn(in vap.OpenAppDesktopInput, ws *websocket.Conn) (err error) {
	var (
		isExist       bool
		guacdTunnel   *gua.Tunnel
		se            tables.VapSession
		configuration *gua.Configuration
		host          tables.VapHost
	)
	// 查询数据库中的会话记录，因为前端在发送websocket前已经发送了一次ajax通知服务端写入会话记录了,现在发起ws时需要更新其中的状态
	se, err = dao.SessionRepository.FindById(in.HostSessionId)
	if err != nil {
		return err
	}
	host, err = dao.HostRepository.FindHostById(se.HostId)
	if err != nil {
		return err
	}
	configuration = s.setConfig(se, in.Width, in.Height, in.Dpi)
	switch host.Origin {
	case tables.AppHostOriginForClient:
		// 计划内主机连接时需要依赖launcher
		configuration.SetParameter(gua.StaticChannels, enum.LauncherVisualChannelName)
		configuration.SetParameter(gua.RemoteApp, enum.LauncherPath)
	}
	logrus.Debugf("[%v] 新建 guacd 会话, guacd=%v, asset=%v", in.HostSessionId, global.Conf.GuacdAddr,
		fmt.Sprintf("%s:%s", configuration.GetParameter("hostname"), configuration.GetParameter("port")))
	logrus.Debugf("连接参数=%+v", configuration.Parameters)
	guacdTunnel, err = gua.NewTunnel(global.Conf.GuacdAddr, configuration)
	if err != nil {
		logrus.Debugf("[%v] 建立连接失败: %v", in.HostSessionId, err.Error())
		s.disconnect(ws, gua.NewTunnelError, err.Error())
		return err
	}
	// 更新数据库中的会话状态
	se.Width = in.Width
	se.Height = in.Height
	isExist = gua.GuaSessionManager.IsExist(in.HostSessionId)
	if in.IsReuse && isExist {
		logrus.Debugln("复用会话")
	} else {
		se.ConnectionId = guacdTunnel.UUID
		se.HoldStatus = tables.VaSessionHoldForHolding
		se.Status = tables.VaSessionStatusForInit
		se.ConnectedTime = time.Now().Unix()
		logrus.Debugln("新建会话，将会话纳入管理")
		// 将相关连接参数封装到 session对象中
		nextSession := &gua.Session{
			ID:          in.HostSessionId,
			Protocol:    se.Protocol,
			Mode:        enum.Guacd,
			WebSocket:   ws,
			GuacdTunnel: guacdTunnel,
		}
		// 将 session对象保存到全局对象中
		nextSession.Observer = gua.NewObserver(in.HostSessionId)
		gua.GuaSessionManager.Add <- nextSession
		// 同时开启一个协程，检测这个会话的状态变化
		go nextSession.Observer.Start()
	}
	if err = dao.SessionRepository.UpdateById(&se); err != nil {
		return err
	}
	// 创建guacamoleHandler ，它提供了两个协程，用来读写 ws 和 tunnel 中的数据
	guacamoleHandler := gua.NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()
	// 开启循环，读取websocket中的信息并发送到tunnel中，消息不是直接发送，而是通过 channel
	// java中通过lock来实现有序发送，go中的channel消息默认就是有系的。
	// 如果ws或者tunnel中读取消息时发生错误，则其中一方已经断开，执行关闭会话的操作,即：关闭连接，修改数据库中的会话状态。
	for {
		var message []byte
		_, message, err = ws.ReadMessage()
		if err != nil {
			guacamoleHandler.Stop()
			return nil
		}
		if string(message) == "10.disconnect;" {
			// 不接受前端发送的销毁信号，rdp断开、相关的会话销毁只能由go监听到会话销毁后移除
			// 必须要周期性发送 ping信号，否则guacd会判定读超时，连接会在1s左右被移除
			go func() {
				for _ = range time.Tick(time.Second) {
					instruction := gua.NewInstruction("ping", fmt.Sprintf("%d", time.Now().Unix()))
					_, err = guacdTunnel.WriteAndFlush([]byte(instruction.String()))
					if err != nil {
						logrus.Error(err)
						return
					}

				}

			}()
		} else {
			_, err = guacdTunnel.WriteAndFlush(message)
			if err != nil {
				// todo 操作数据库
				//service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
				return nil
			}
		}
	}
}

func (s guaService) ConnForMonitor(sid int64, ws *websocket.Conn) (err error) {
	var (
		se tables.VapSession
	)
	if se, err = dao.SessionRepository.FindById(sid); err != nil {
		return err
	}
	if se.HoldStatus != tables.VaSessionHoldForHolding {
		s.disconnect(ws, gua.AssetNotActive, "会话离线")
		err = errors.New("会话已离线")
		return
	}
	configuration := gua.NewConfiguration()
	configuration.ConnectionID = se.ConnectionId
	configuration.SetParameter("width", se.Width)
	configuration.SetParameter("height", se.Width)
	configuration.SetParameter("dpi", "96")

	logrus.Debugf("[%v] 新建 guacd 会话, guacd=%v, asset=%+v", sid, global.Conf.GuacdAddr, configuration)

	guacdTunnel, err := gua.NewTunnel(global.Conf.GuacdAddr, configuration)
	if err != nil {
		s.disconnect(ws, gua.NewTunnelError, err.Error())
		logrus.Debugf("[%v] 建立连接失败: %v", sid, err.Error())
		return err
	}

	nextSession := &gua.Session{
		ID:          sid,
		Protocol:    se.Protocol,
		Mode:        enum.Guacd,
		WebSocket:   ws,
		GuacdTunnel: guacdTunnel,
	}

	// 要监控会话
	forObsSession := gua.GuaSessionManager.GetById(sid)
	if forObsSession == nil {
		s.disconnect(ws, gua.NotFoundSession, "获取会话失败")
		err = errors.New("获取会话失败")
		return
	}
	nextSession.ID = id.GetSnowId()
	forObsSession.Observer.Add <- nextSession
	logrus.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sid, configuration.ConnectionID, nextSession.ID, se.ConnectionId)

	guacamoleHandler := gua.NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()
	for {
		_, message, err := ws.ReadMessage()
		if err != nil {
			logrus.Debugf("[%v:%v] WebSocket已关闭, %v", sid, configuration.ConnectionID, err.Error())
			// guacdTunnel.Read() 会阻塞，所以要先把guacdTunnel客户端关闭，才能退出Guacd循环
			_ = guacdTunnel.Close()

			observerId := nextSession.ID
			forObsSession.Observer.Del <- observerId
			logrus.Debugf("[%v:%v] 观察者[%v]退出会话", sid, configuration.ConnectionID, observerId)
			guacamoleHandler.Stop()
			return nil
		}
		_, err = guacdTunnel.WriteAndFlush(message)
		if err != nil {
			// todo 操作数据库
			// service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
			return nil
		}
	}

}

func (s guaService) setConfig(se tables.VapSession, width, height, dpi string) (configuration *gua.Configuration) {
	configuration = gua.NewConfiguration()
	configuration.Protocol = se.Protocol
	configuration.SetParameter("width", width)
	configuration.SetParameter("height", height)
	configuration.SetParameter("dpi", dpi)
	propertyMap := dao.SettingsRepository.FindGlobalGuaPropertiesMap() //全局配置
	switch configuration.Protocol {
	case "rdp":
		configuration.SetParameter(gua.Username, se.WinAccount)
		configuration.SetParameter(gua.Password, se.WinPwd)
		configuration.SetParameter(gua.Hostname, se.HostIp)
		configuration.SetParameter(gua.Port, strconv.Itoa(se.ConnectPort))

		params := []string{gua.Security, gua.IgnoreCert, gua.ResizeMethod, gua.DisableAudio, gua.EnableAudioInput, gua.RecordingPath, gua.CreateRecordingPath}
		s.setBaseConfig(params, propertyMap, configuration)
		if configuration.GetParameter(gua.RecordingPath) != "" {
			configuration.SetParameter(gua.RecordingPath, fmt.Sprintf("%s/%d", configuration.GetParameter(gua.RecordingPath), se.Id))
		}

	case "ssh":
		configuration.SetParameter(gua.FontSize, propertyMap[gua.FontSize])
		configuration.SetParameter(gua.FontName, propertyMap[gua.FontName])
		configuration.SetParameter(gua.ColorScheme, propertyMap[gua.ColorScheme])
		configuration.SetParameter(gua.Backspace, propertyMap[gua.Backspace])
		configuration.SetParameter(gua.TerminalType, propertyMap[gua.TerminalType])
		configuration.SetParameter("username", se.WinAccount)
		configuration.SetParameter("password", se.WinPwd)
	case "vnc":
		configuration.SetParameter("username", se.WinAccount)
		configuration.SetParameter("password", se.WinPwd)
	case "telnet":
		configuration.SetParameter("username", se.WinAccount)
		configuration.SetParameter("password", se.WinPwd)
		configuration.SetParameter(gua.FontSize, propertyMap[gua.FontSize])
		configuration.SetParameter(gua.FontName, propertyMap[gua.FontName])
		configuration.SetParameter(gua.ColorScheme, propertyMap[gua.ColorScheme])
		configuration.SetParameter(gua.Backspace, propertyMap[gua.Backspace])
		configuration.SetParameter(gua.TerminalType, propertyMap[gua.TerminalType])
	case "kubernetes":
		configuration.SetParameter(gua.FontSize, propertyMap[gua.FontSize])
		configuration.SetParameter(gua.FontName, propertyMap[gua.FontName])
		configuration.SetParameter(gua.ColorScheme, propertyMap[gua.ColorScheme])
		configuration.SetParameter(gua.Backspace, propertyMap[gua.Backspace])
		configuration.SetParameter(gua.TerminalType, propertyMap[gua.TerminalType])
	default:
		logrus.Errorf("Unsupported protocol: %s", configuration.Protocol)
	}
	return
}

// setBaseConfig 写入全局基础配置功能
func (s guaService) setBaseConfig(params []string, propertyMap map[string]string, configuration *gua.Configuration) {
	for _, item := range params {
		if propertyMap[item] != "" {
			configuration.SetParameter(item, propertyMap[item])

		}
	}
}

func (s guaService) disconnect(ws *websocket.Conn, code int, reason string) {
	// guacd 无法处理中文字符，所以进行了base64编码。
	encodeReason := base64.StdEncoding.EncodeToString([]byte(reason))
	err := gua.NewInstruction("error", encodeReason, strconv.Itoa(code))
	_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
	disconnect := gua.NewInstruction("disconnect")
	_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
}

func (s guaService) Sh(in vap.ShArgus, ws *websocket.Conn) (err error) {
	var (
		h        tables.VapShHost
		username string
		password string
	)
	h, err = dao.ShHostDao.FindById(in.HostId)
	if err != nil {
		return
	}
	switch in.AuthType {
	case 1:
		username = h.Username
		password = h.Password
	case 2:
		username = in.Username
		password = in.Password
	}
	conf, err := s.getShConfig(h, username, password, in.Width, in.Height, in.Dpi, "ssh")
	if err != nil {
		return
	}
	logrus.Debugf("sh conf: %+v", conf)
	guacdTunnel, err := gua.NewTunnel(global.Conf.GuacdAddr, conf)
	if err != nil {
		return
	}
	guacamoleHandler := gua.NewGuacamoleHandler(ws, guacdTunnel)
	guacamoleHandler.Start()
	for {
		var message []byte
		_, message, err = ws.ReadMessage()
		if err != nil {
			guacamoleHandler.Stop()
			return nil
		}
		if string(message) == "10.disconnect;" {
			// 不接受前端发送的销毁信号，rdp断开、相关的会话销毁只能由go监听到会话销毁后移除
			// 必须要周期性发送 ping信号，否则guacd会判定读超时，连接会在1s左右被移除
			go func() {
				for _ = range time.Tick(time.Second) {
					instruction := gua.NewInstruction("ping", fmt.Sprintf("%d", time.Now().Unix()))
					_, err = guacdTunnel.WriteAndFlush([]byte(instruction.String()))
					if err != nil {
						logrus.Error(err)
						return
					}

				}

			}()
		} else {
			_, err = guacdTunnel.WriteAndFlush(message)
			if err != nil {
				// todo 操作数据库
				//service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
				return nil
			}
		}
	}
}

func (s guaService) getShConfig(h tables.VapShHost, username, password, width, height, dpi, protocol string) (conf *gua.Configuration, err error) {
	if protocol != "ssh" {
		err = fmt.Errorf("protocol not supported:%s", protocol)
		return
	}
	conf = gua.NewConfiguration()
	conf.Protocol = protocol
	conf.SetParameter(gua.Hostname, h.Ip)
	conf.SetParameter(gua.Port, strconv.Itoa(h.ConnPort))
	conf.SetParameter("width", width)
	conf.SetParameter("height", height)
	conf.SetParameter("dpi", dpi)
	propertyMap := dao.SettingsRepository.FindGlobalGuaPropertiesMap() //全局配置
	conf.SetParameter(gua.FontSize, propertyMap[gua.FontSize])
	conf.SetParameter(gua.FontName, propertyMap[gua.FontName])
	conf.SetParameter(gua.ColorScheme, propertyMap[gua.ColorScheme])
	conf.SetParameter(gua.Backspace, propertyMap[gua.Backspace])
	conf.SetParameter(gua.TerminalType, propertyMap[gua.TerminalType])
	conf.SetParameter("username", username)
	conf.SetParameter("password", password)
	return conf, nil
}
